package com.zf.common.redis.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 流水号生成工具
 */
public class SerialNumberGenerator {
    private static final long ONE_DAY = 1L;

    /**
     * `incr key`，`expire key second-time` 脚本
     */
    private static final String incrLuaScript =
            "local key = KEYS[1]\n" +
                    "local time = tonumber(ARGV[1])\n" +
                    "local current = redis.call('incr', key)\n" +
                    "redis.call('expire', key, time)\n" +
                    "return tonumber(current)";

    /**
     * `incrby key delta`，`expire key second-time` 脚本
     */
    private static final String incrByDeltaLuaScript =
            "local key = KEYS[1]\n" +
                    "local delta = tonumber(ARGV[1])\n" +
                    "local time = tonumber(ARGV[2])\n" +
                    "local current = redis.call('incrby', key, delta)\n" +
                    "redis.call('expire', key, time)\n" +
                    "return tonumber(current)";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 自动生成流水号, 每天重新从1开始
     *
     * @param redisKey     redis key
     * @param numberPrefix 序列号前缀
     * @param pattern      日期格式
     * @param length       流水号位数
     * @return 生成的流水号: 序列号前缀+格式化日期+自增流水号
     */
    public String getNextSerialNo(String redisKey, String numberPrefix, String pattern, int length) {
        // 当前日期字符串
        String dateStr = DateTimeFormatter.ofPattern(pattern).format(LocalDate.now());
        // 生成序号值
        long nextLongValue = getNextLongValue(redisKey, 1, dateStr);
        // 拼接序号
        return numberPrefix + dateStr + String.format("%0" + length + "d", nextLongValue);
    }

    /**
     * 按照指定的步长生成序号值: 每天重新开始
     *
     * @param redisKey redis kye
     * @param delta    步长
     * @param dateStr  年月日字符串
     */
    public long getNextLongValue(String redisKey, int delta, String dateStr) {
        // “redis key + 日期” 作为新的 redis key；
        // 这样每天的key都是新的，序号也会从1开始；
        // 只需要让旧的key在24h后过期即可
        redisKey = redisKey + dateStr;
        // 产生新的序列号
        Long nextLongValue = redisTemplate.opsForValue().increment(redisKey, delta);
        // 设置 key 在24h后过期
        redisTemplate.expire(redisKey, 24 * 60 * 60, TimeUnit.SECONDS);
        return nextLongValue.longValue();
    }

    /**
     * 自动生成流水号, 每天重新从1开始；使用Lua脚本实现
     *
     * @param redisKey     redis key
     * @param numberPrefix 序列号前缀
     * @param pattern      日期格式
     * @param length       流水号位数
     * @return 生成的流水号: 序列号前缀+格式化日期+自增流水号
     */
    public String getNextSerialNoByLua(String redisKey, String numberPrefix, String pattern, int length) {
        // 当前日期
        LocalDateTime now = LocalDateTime.now();
        String dateStr = DateTimeFormatter.ofPattern(pattern).format(now);
        // 执行 lua脚本，产生新的序列号值
        long serialNumber = getNextLongValueByLua(redisKey, 1, now);
        // 拼接序号
        return numberPrefix + dateStr + String.format("%0" + length + "d", serialNumber);
    }

    /**
     * 按照指定的步长生成序号值: 每天重新开始；使用Lua脚本实现
     *
     * @param redisKey redis kye
     * @param delta    步长
     * @param now      当前时间
     */
    public long getNextLongValueByLua(String redisKey, int delta, LocalDateTime now) {
        //读取 lua 脚本, 必须指定返回类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(incrByDeltaLuaScript, Long.class);
        // redisScript.setScriptText(incrByDeltaLuaScript);
        // //必须设置
        // redisScript.setResultType(Long.class);

        // redis中的key
        List<String> keys = Collections.singletonList(redisKey);

        // 第二天的开始时间
        LocalDateTime expireDate = now.plusDays(ONE_DAY).toLocalDate().atStartOfDay();
        // 距离过期时间的秒数
        long seconds = Duration.between(now, expireDate).getSeconds();

        //执行 lua脚本，产生新的序列号
        Long serialNumber = redisTemplate.execute(redisScript, keys, delta, seconds);
        return serialNumber.longValue();
    }

}
